/**
 * @file sylixosfb.c
 *
 */

/*********************
 *      INCLUDES
 *********************/
#define  __SYLIXOS_KERNEL
#include <SylixOS.h>
#include "sylixosfb.h"
#if USE_SYLIXOS_FBDEV

#include <stdlib.h>
#include <unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>

//#include "libg2d.h"

/*********************
 *      DEFINES
 *********************/
#ifndef FBDEV_PATH
#define FBDEV_PATH  "/dev/fb0"
#endif

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *      STRUCTURES
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p);
static void fbdev_wait_cb(struct _lv_disp_drv_t * disp_drv);

/**********************
 *  STATIC VARIABLES
 **********************/
static LW_GM_VARINFO vinfo;
static LW_GM_SCRINFO finfo;
static char *fbp = 0;
static char *fbp_phy = 0;
static long int screensize = 0;
static int fbfd = 0;
static char *draw_buf = 0;
static char *draw_buf2 = 0;

/**********************
 *      MACROS
 **********************/
#define NORMAL_MODE      1 // memcpy flush
#define G2D_SINGLE_MODE  2 // g2d hardware flush
#define G2D_DOUBLE_MODE  3 // g2d hardware flush,use double buffer

#define DISPLAY_FLUSH_MODE     (NORMAL_MODE)

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

void fbdev_init(void)
{
    /*-------------------------
     * Initialize your display
     * -----------------------*/

    // Open the file for reading and writing
    fbfd = open(FBDEV_PATH, O_RDWR);
    if(fbfd == -1) {
        perror("Error: cannot open framebuffer device");
        return;
    }
    printf("The framebuffer device was opened successfully.\n");

#if (DISPLAY_FLUSH_MODE != NORMAL_MODE)
    int ret, flag;

#if (DISPLAY_FLUSH_MODE == G2D_SINGLE_MODE)
    flag = O_RDWR;
#else
    flag = O_RDWR | O_NONBLOCK;
#endif
    ret = g2d_open(flag);
    if (ret < 0) {
        fprintf(stderr, "open /dev/g2d failed.\n");
        return;
    }
#endif

    // Get fixed screen information
    if(ioctl(fbfd, LW_GM_GET_SCRINFO, &finfo) == -1) {
        perror("Error reading fixed information");
        return;
    }

    // Get variable screen information
    if(ioctl(fbfd, LW_GM_GET_VARINFO, &vinfo) == -1) {
        perror("Error reading variable information");
        return;
    }

    printf("%ldx%ld, %ldbpp\n", vinfo.GMVI_ulXRes, vinfo.GMVI_ulYRes, vinfo.GMVI_ulBitsPerPixel);

    // Figure out the size of the screen in bytes
    screensize =  finfo.GMSI_stMemSizePerLine * vinfo.GMVI_ulYRes;

    // Map the device to memory
    fbp = (char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);
    if((intptr_t)fbp == -1) {
        perror("Error: failed to map framebuffer device to memory");
        return;
    }
    memset(fbp, 0, screensize);

    fbp_phy = finfo.GMSI_pcMem;

    printf("The framebuffer device was mapped to memory successfully.\n");

    /*-----------------------------
     * Create a buffer for drawing
     *----------------------------*/

    /**
     * LVGL requires a buffer where it internally draws the widgets.
     * Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display.
     * The buffer has to be greater than 1 display row
     *
     * There are 3 buffering configurations:
     * 1. Create ONE buffer:
     *      LVGL will draw the display's content here and writes it to your display
     *
     * 2. Create TWO buffer:
     *      LVGL will draw the display's content to a buffer and writes it your display.
     *      You should use DMA to write the buffer's content to the display.
     *      It will enable LVGL to draw the next part of the screen to the other buffer while
     *      the data is being sent form the first buffer. It makes rendering and flushing parallel.
     *
     * 3. Double buffering
     *      Set 2 screens sized buffers and set disp_drv.full_refresh = 1.
     *      This way LVGL will always provide the whole rendered screen in `flush_cb`
     *      and you only need to change the frame buffer's address.
     */

    /* Example for 1) */
    static lv_disp_draw_buf_t draw_buf_dsc;
#if (DISPLAY_FLUSH_MODE == NORMAL_MODE)
    draw_buf = malloc(screensize);                          /*A buffer for screen*/
#else
    ULONG   ulFlags = LW_VMM_FLAG_RDWR;
//    ULONG   ulFlags = LW_VMM_FLAG_DMA;
    draw_buf  = API_VmmDmaAllocAlignWithFlags(screensize, LW_CFG_VMM_PAGE_SIZE, ulFlags);
#if (DISPLAY_FLUSH_MODE == G2D_DOUBLE_MODE)
    draw_buf2 = API_VmmDmaAllocAlignWithFlags(screensize, LW_CFG_VMM_PAGE_SIZE, ulFlags);
#endif
#endif

    lv_disp_draw_buf_init(&draw_buf_dsc, draw_buf, draw_buf2, vinfo.GMVI_ulXRes * vinfo.GMVI_ulYRes);   /*Initialize the display buffer*/

#if 0
    /* Example for 2) */
    static lv_disp_draw_buf_t draw_buf_dsc_2;
    static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10];                        /*A buffer for 10 rows*/
    static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10];                        /*An other buffer for 10 rows*/
    lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10);   /*Initialize the display buffer*/

    /* Example for 3) also set disp_drv.full_refresh = 1 below*/
    static lv_disp_draw_buf_t draw_buf_dsc_3;
    static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES];            /*A screen sized buffer*/
    static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES];            /*Another screen sized buffer*/
    lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2, MY_DISP_VER_RES * LV_VER_RES_MAX);   /*Initialize the display buffer*/
#endif

    /*-----------------------------------
     * Register the display in LVGL
     *----------------------------------*/

    static lv_disp_drv_t disp_drv;                         /*Descriptor of a display driver*/
    lv_disp_drv_init(&disp_drv);                    /*Basic initialization*/

    /*Set up the functions to access to your display*/

    /*Set the resolution of the display*/
    disp_drv.hor_res = vinfo.GMVI_ulXRes;
    disp_drv.ver_res = vinfo.GMVI_ulYRes;

    /*Used to copy the buffer's content to the display*/
    disp_drv.flush_cb = fbdev_flush;
    disp_drv.wait_cb  = fbdev_wait_cb;

    /*Set a display buffer*/
    disp_drv.draw_buf = &draw_buf_dsc;

    /*Required for Example 3)*/
//    disp_drv.full_refresh = 1;

    /* Fill a memory array with a color if you have GPU.
     * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL.
     * But if you have a different GPU you can use with this callback.*/
    //disp_drv.gpu_fill_cb = gpu_fill;

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}

void fbdev_exit(void)
{
    close(fbfd);
}

/**
 * Flush a buffer to the marked area
 * @param drv pointer to driver where this function belongs
 * @param area an area where to copy `color_p`
 * @param color_p an array of pixel to copy to the `area` part of the screen
 */
static void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{
    if(fbp == NULL ||
            area->x2 < 0 ||
            area->y2 < 0 ||
            area->x1 > (int32_t)vinfo.GMVI_ulXRes - 1 ||
            area->y1 > (int32_t)vinfo.GMVI_ulYRes - 1) {
        lv_disp_flush_ready(drv);
        return;
    }

#if (DISPLAY_FLUSH_MODE == NORMAL_MODE)

    /*Truncate the area to the screen*/
    int32_t act_x1 = area->x1 < 0 ? 0 : area->x1;
    int32_t act_y1 = area->y1 < 0 ? 0 : area->y1;
    int32_t act_x2 = area->x2 > (int32_t)vinfo.GMVI_ulXRes - 1 ? (int32_t)vinfo.GMVI_ulXRes - 1 : area->x2;
    int32_t act_y2 = area->y2 > (int32_t)vinfo.GMVI_ulYRes - 1 ? (int32_t)vinfo.GMVI_ulYRes - 1 : area->y2;

    lv_coord_t w = lv_area_get_width(area);
    long int location = 0;
    long int byte_location = 0;
    unsigned char bit_location = 0;

    /*32 or 24 bit per pixel*/
    if(vinfo.GMVI_ulBitsPerPixel == 32 || vinfo.GMVI_ulBitsPerPixel == 24) {
        uint32_t * fbp32 = (uint32_t *)fbp;
        int32_t y;
        for(y = act_y1; y <= act_y2; y++) {
            location = (act_x1 + vinfo.GMVI_ulXOffset) + (y + vinfo.GMVI_ulYOffset) * finfo.GMSI_stMemSizePerLine / 4;
            memcpy(&fbp32[location], (uint32_t *)color_p, (act_x2 - act_x1 + 1) * 4);
            color_p += w;
        }
    }
    /*16 bit per pixel*/
    else if(vinfo.GMVI_ulBitsPerPixel == 16) {
        uint16_t * fbp16 = (uint16_t *)fbp;
        int32_t y;
        for(y = act_y1; y <= act_y2; y++) {
            location = (act_x1 + vinfo.GMVI_ulXOffset) + (y + vinfo.GMVI_ulYOffset) * finfo.GMSI_stMemSizePerLine / 2;
            memcpy(&fbp16[location], (uint32_t *)color_p, (act_x2 - act_x1 + 1) * 2);
            color_p += w;
        }
    }
    /*8 bit per pixel*/
    else if(vinfo.GMVI_ulBitsPerPixel == 8) {
        uint8_t * fbp8 = (uint8_t *)fbp;
        int32_t y;
        for(y = act_y1; y <= act_y2; y++) {
            location = (act_x1 + vinfo.GMVI_ulXOffset) + (y + vinfo.GMVI_ulYOffset) * finfo.GMSI_stMemSizePerLine;
            memcpy(&fbp8[location], (uint32_t *)color_p, (act_x2 - act_x1 + 1));
            color_p += w;
        }
    }
    /*1 bit per pixel*/
    else if(vinfo.GMVI_ulBitsPerPixel == 1) {
        uint8_t * fbp8 = (uint8_t *)fbp;
        int32_t x;
        int32_t y;
        for(y = act_y1; y <= act_y2; y++) {
            for(x = act_x1; x <= act_x2; x++) {
                location = (x + vinfo.GMVI_ulXOffset) + (y + vinfo.GMVI_ulYOffset) * vinfo.GMVI_ulXRes;
                byte_location = location / 8; /* find the byte we need to change */
                bit_location = location % 8; /* inside the byte found, find the bit we need to change */
                fbp8[byte_location] &= ~(((uint8_t)(1)) << bit_location);
                fbp8[byte_location] |= ((uint8_t)(color_p->full)) << bit_location;
                color_p++;
            }

            color_p += area->x2 - act_x2;
        }
    } else {
        /*Not supported bit per pixel*/
    }

    //May be some direct update command is required
    //ret = ioctl(state->fd, FBIO_UPDATE, (unsigned long)((uintptr_t)rect));
#else
    API_CacheFlush(DATA_CACHE, draw_buf, screensize);

    g2d_bitblt((unsigned long)color_p, 0, 0,
               (unsigned long)fbp_phy, 32, area->x1, area->y1,
               lv_area_get_width(area), lv_area_get_height(area),
               lv_area_get_width(area), lv_area_get_height(area),
               vinfo.GMVI_ulXRes, vinfo.GMVI_ulYRes);
#endif

#if (DISPLAY_FLUSH_MODE != G2D_DOUBLE_MODE)
    lv_disp_flush_ready(drv);
#endif
}

static void fbdev_wait_cb(struct _lv_disp_drv_t * disp_drv)
{
#if (DISPLAY_FLUSH_MODE == G2D_DOUBLE_MODE)
    g2d_waitfinish();
    lv_disp_flush_ready(disp_drv);
#endif
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

#endif
